From 7e543f4747914f280454f09b070ead500fb72277 Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Thu, 18 Apr 2024 20:18:51 +0200 Subject: Add Ecosia Provider, Add OpenaiAccount alias Use AsyncClient in API, add web_search parameter in API Improve error messages in Openai --- examples/ecosia.py | 18 ++++++++++++ g4f/Provider/Ecosia.py | 47 ++++++++++++++++++++++++++++++++ g4f/Provider/You.py | 1 - g4f/Provider/__init__.py | 3 +- g4f/Provider/needs_auth/OpenRouter.py | 2 +- g4f/Provider/needs_auth/Openai.py | 11 ++++++-- g4f/Provider/needs_auth/OpenaiAccount.py | 7 +++++ g4f/Provider/needs_auth/OpenaiChat.py | 28 +++++++++++-------- g4f/Provider/needs_auth/__init__.py | 3 +- g4f/api/__init__.py | 29 ++++++++++---------- g4f/gui/client/index.html | 8 +++--- g4f/providers/base_provider.py | 8 ++++-- 12 files changed, 125 insertions(+), 40 deletions(-) create mode 100644 examples/ecosia.py create mode 100644 g4f/Provider/Ecosia.py create mode 100644 g4f/Provider/needs_auth/OpenaiAccount.py diff --git a/examples/ecosia.py b/examples/ecosia.py new file mode 100644 index 00000000..5a2ae520 --- /dev/null +++ b/examples/ecosia.py @@ -0,0 +1,18 @@ +import asyncio +import g4f +from g4f.client import AsyncClient + +async def main(): + client = AsyncClient( + provider=g4f.Provider.Ecosia, + ) + async for chunk in client.chat.completions.create( + [{"role": "user", "content": "happy dogs on work. write some lines"}], + g4f.models.default, + stream=True, + green=True, + ): + print(chunk.choices[0].delta.content or "", end="") + print(f"\nwith {chunk.model}") + +asyncio.run(main()) \ No newline at end of file diff --git a/g4f/Provider/Ecosia.py b/g4f/Provider/Ecosia.py new file mode 100644 index 00000000..1cae3560 --- /dev/null +++ b/g4f/Provider/Ecosia.py @@ -0,0 +1,47 @@ + +from __future__ import annotations + +import base64 +import json +from aiohttp import ClientSession, BaseConnector + +from ..typing import AsyncResult, Messages +from ..requests.raise_for_status import raise_for_status +from .base_provider import AsyncGeneratorProvider, ProviderModelMixin +from .helper import get_connector + +class Ecosia(AsyncGeneratorProvider, ProviderModelMixin): + url = "https://www.ecosia.org" + working = True + supports_gpt_35_turbo = True + default_model = "gpt-3.5-turbo-0125" + model_aliases = {"gpt-3.5-turbo": "gpt-3.5-turbo-0125"} + + @classmethod + async def create_async_generator( + cls, + model: str, + messages: Messages, + connector: BaseConnector = None, + green: bool = False, + proxy: str = None, + **kwargs + ) -> AsyncResult: + cls.get_model(model) + headers = { + "authority": "api.ecosia.org", + "accept": "*/*", + "origin": cls.url, + "referer": f"{cls.url}/", + "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36", + } + async with ClientSession(headers=headers, connector=get_connector(connector, proxy)) as session: + data = { + "messages": base64.b64encode(json.dumps(messages).encode()).decode() + } + api_url = f"https://api.ecosia.org/v2/chat/?sp={'eco' if green else 'productivity'}" + async with session.post(api_url, json=data) as response: + await raise_for_status(response) + async for chunk in response.content.iter_any(): + if chunk: + yield chunk.decode(errors="ignore") \ No newline at end of file diff --git a/g4f/Provider/You.py b/g4f/Provider/You.py index 3ebd40f2..3d3a3513 100644 --- a/g4f/Provider/You.py +++ b/g4f/Provider/You.py @@ -132,7 +132,6 @@ class You(AsyncGeneratorProvider, ProviderModelMixin): @classmethod async def get_cookies(cls, client: StreamSession) -> Cookies: - if not cls._cookies or cls._cookies_used >= 5: cls._cookies = await cls.create_cookies(client) cls._cookies_used = 0 diff --git a/g4f/Provider/__init__.py b/g4f/Provider/__init__.py index ea64f80a..d5913e3c 100644 --- a/g4f/Provider/__init__.py +++ b/g4f/Provider/__init__.py @@ -23,9 +23,11 @@ from .ChatgptFree import ChatgptFree from .ChatgptNext import ChatgptNext from .ChatgptX import ChatgptX from .Cnote import Cnote +from .Cohere import Cohere from .DeepInfra import DeepInfra from .DeepInfraImage import DeepInfraImage from .DuckDuckGo import DuckDuckGo +from .Ecosia import Ecosia from .Feedough import Feedough from .FlowGpt import FlowGpt from .FreeChatgpt import FreeChatgpt @@ -46,7 +48,6 @@ from .ReplicateImage import ReplicateImage from .Vercel import Vercel from .WhiteRabbitNeo import WhiteRabbitNeo from .You import You -from .Cohere import Cohere import sys diff --git a/g4f/Provider/needs_auth/OpenRouter.py b/g4f/Provider/needs_auth/OpenRouter.py index 773d9203..7945784a 100644 --- a/g4f/Provider/needs_auth/OpenRouter.py +++ b/g4f/Provider/needs_auth/OpenRouter.py @@ -9,7 +9,7 @@ class OpenRouter(Openai): label = "OpenRouter" url = "https://openrouter.ai" working = True - default_model = "openrouter/auto" + default_model = "mistralai/mistral-7b-instruct:free" @classmethod def get_models(cls): diff --git a/g4f/Provider/needs_auth/Openai.py b/g4f/Provider/needs_auth/Openai.py index 80318f6d..f73c1011 100644 --- a/g4f/Provider/needs_auth/Openai.py +++ b/g4f/Provider/needs_auth/Openai.py @@ -56,6 +56,7 @@ class Openai(AsyncGeneratorProvider, ProviderModelMixin): await raise_for_status(response) if not stream: data = await response.json() + cls.raise_error(data) choice = data["choices"][0] if "content" in choice["message"]: yield choice["message"]["content"].strip() @@ -70,8 +71,7 @@ class Openai(AsyncGeneratorProvider, ProviderModelMixin): if chunk == b"[DONE]": break data = json.loads(chunk) - if "error_message" in data: - raise ResponseError(data["error_message"]) + cls.raise_error(data) choice = data["choices"][0] if "content" in choice["delta"] and choice["delta"]["content"]: delta = choice["delta"]["content"] @@ -89,6 +89,13 @@ class Openai(AsyncGeneratorProvider, ProviderModelMixin): if "finish_reason" in choice and choice["finish_reason"] is not None: return FinishReason(choice["finish_reason"]) + @staticmethod + def raise_error(data: dict): + if "error_message" in data: + raise ResponseError(data["error_message"]) + elif "error" in data: + raise ResponseError(f'Error {data["error"]["code"]}: {data["error"]["message"]}') + @classmethod def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict: return { diff --git a/g4f/Provider/needs_auth/OpenaiAccount.py b/g4f/Provider/needs_auth/OpenaiAccount.py new file mode 100644 index 00000000..5c90b1de --- /dev/null +++ b/g4f/Provider/needs_auth/OpenaiAccount.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from .OpenaiChat import OpenaiChat + +class OpenaiAccount(OpenaiChat): + label = "OpenAI ChatGPT with Account" + needs_auth = True \ No newline at end of file diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py index b34daa3e..07620c80 100644 --- a/g4f/Provider/needs_auth/OpenaiChat.py +++ b/g4f/Provider/needs_auth/OpenaiChat.py @@ -23,7 +23,7 @@ except ImportError: from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin from ...webdriver import get_browser -from ...typing import AsyncResult, Messages, Cookies, ImageType, Union, AsyncIterator +from ...typing import AsyncResult, Messages, Cookies, ImageType, AsyncIterator from ...requests import get_args_from_browser, raise_for_status from ...requests.aiohttp import StreamSession from ...image import to_image, to_bytes, ImageResponse, ImageRequest @@ -35,7 +35,7 @@ from ... import debug class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): """A class for creating and managing conversations with OpenAI chat service""" - lebel = "OpenAI ChatGPT" + label = "OpenAI ChatGPT" url = "https://chat.openai.com" working = True supports_gpt_35_turbo = True @@ -295,7 +295,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): model: str, messages: Messages, proxy: str = None, - timeout: int = 120, + timeout: int = 180, api_key: str = None, cookies: Cookies = None, auto_continue: bool = False, @@ -348,7 +348,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): if api_key is not None: cls._set_api_key(api_key) - if cls.default_model is None and cls._api_key is not None: + if cls.default_model is None and (not cls.needs_auth or cls._api_key is not None): try: if not model: cls.default_model = cls.get_model(await cls.get_default_model(session, cls._headers)) @@ -368,12 +368,12 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): arkose_token, api_key, cookies = await getArkoseAndAccessToken(proxy) cls._create_request_args(cookies) cls._set_api_key(api_key) - except NoValidHarFileError: + except NoValidHarFileError as e: ... if cls._api_key is None: - if debug.logging: - print("Getting access token with nodriver.") await cls.nodriver_access_token() + if cls._api_key is None and cls.needs_auth: + raise e cls.default_model = cls.get_model(await cls.get_default_model(session, cls._headers)) async with session.post( @@ -589,10 +589,11 @@ this.fetch = async (url, options) => { user_data_dir = user_config_dir("g4f-nodriver") except: user_data_dir = None - + if debug.logging: + print(f"Open nodriver with user_dir: {user_data_dir}") browser = await uc.start(user_data_dir=user_data_dir) page = await browser.get("https://chat.openai.com/") - while await page.query_selector("#prompt-textarea") is None: + while await page.find("[id^=headlessui-menu-button-]") is None: await asyncio.sleep(1) api_key = await page.evaluate( "(async () => {" @@ -609,8 +610,9 @@ this.fetch = async (url, options) => { for c in await page.browser.cookies.get_all(): if c.domain.endswith("chat.openai.com"): cookies[c.name] = c.value + user_agent = await page.evaluate("window.navigator.userAgent") await page.close() - cls._create_request_args(cookies) + cls._create_request_args(cookies, user_agent) cls._set_api_key(api_key) @classmethod @@ -662,7 +664,7 @@ this.fetch = async (url, options) => { "content-type": "application/json", "oai-device-id": str(uuid.uuid4()), "oai-language": "en-US", - "sec-ch-ua": "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\"", + "sec-ch-ua": "\"Google Chrome\";v=\"123\", \"Not:A-Brand\";v=\"8\", \"Chromium\";v=\"123\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Linux\"", "sec-fetch-dest": "empty", @@ -675,8 +677,10 @@ this.fetch = async (url, options) => { return "; ".join(f"{k}={v}" for k, v in cookies.items() if k != "access_token") @classmethod - def _create_request_args(cls, cookies: Cookies = None): + def _create_request_args(cls, cookies: Cookies = None, user_agent: str = None): cls._headers = cls.get_default_headers() + if user_agent is not None: + cls._headers["user-agent"] = user_agent cls._cookies = {} if cookies is None else cookies cls._update_cookie_header() diff --git a/g4f/Provider/needs_auth/__init__.py b/g4f/Provider/needs_auth/__init__.py index 7b793223..805d9fca 100644 --- a/g4f/Provider/needs_auth/__init__.py +++ b/g4f/Provider/needs_auth/__init__.py @@ -6,4 +6,5 @@ from .OpenaiChat import OpenaiChat from .Poe import Poe from .Openai import Openai from .Groq import Groq -from .OpenRouter import OpenRouter \ No newline at end of file +from .OpenRouter import OpenRouter +from .OpenaiAccount import OpenaiAccount \ No newline at end of file diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py index 383e22be..8151881e 100644 --- a/g4f/api/__init__.py +++ b/g4f/api/__init__.py @@ -1,7 +1,6 @@ import logging import json import uvicorn -import nest_asyncio from fastapi import FastAPI, Response, Request from fastapi.responses import StreamingResponse, RedirectResponse, HTMLResponse, JSONResponse @@ -9,22 +8,23 @@ from fastapi.exceptions import RequestValidationError from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY from fastapi.encoders import jsonable_encoder from pydantic import BaseModel -from typing import List, Union +from typing import List, Union, Optional import g4f import g4f.debug -from g4f.client import Client +from g4f.client import AsyncClient from g4f.typing import Messages class ChatCompletionsConfig(BaseModel): messages: Messages model: str - provider: Union[str, None] = None + provider: Optional[str] = None stream: bool = False - temperature: Union[float, None] = None - max_tokens: Union[int, None] = None + temperature: Optional[float] = None + max_tokens: Optional[int] = None stop: Union[list[str], str, None] = None - api_key: Union[str, None] = None + api_key: Optional[str] = None + web_search: Optional[bool] = None class Api: def __init__(self, engine: g4f, debug: bool = True, sentry: bool = False, @@ -36,9 +36,7 @@ class Api: if debug: g4f.debug.logging = True - self.client = Client() - - nest_asyncio.apply() + self.client = AsyncClient() self.app = FastAPI() self.routes() @@ -90,7 +88,7 @@ class Api: @self.app.get("/v1/models/{model_name}") async def model_info(model_name: str): try: - model_info = g4f.ModelUtils.convert[model_name] + model_info = g4f.models.ModelUtils.convert[model_name] return JSONResponse({ 'id': model_name, 'object': 'model', @@ -119,17 +117,18 @@ class Api: return Response(content=format_exception(e, config), status_code=500, media_type="application/json") if not config.stream: - return JSONResponse(response.to_json()) + return JSONResponse((await response).to_json()) - def streaming(): + async def streaming(): try: - for chunk in response: + async for chunk in response: yield f"data: {json.dumps(chunk.to_json())}\n\n" except GeneratorExit: pass except Exception as e: logging.exception(e) - yield f'data: {format_exception(e, config)}' + yield f'data: {format_exception(e, config)}\n\n' + yield "data: [DONE]\n\n" return StreamingResponse(streaming(), media_type="text/event-stream") diff --git a/g4f/gui/client/index.html b/g4f/gui/client/index.html index 642925e3..a6c4909b 100644 --- a/g4f/gui/client/index.html +++ b/g4f/gui/client/index.html @@ -134,7 +134,7 @@
- +
@@ -146,12 +146,12 @@
- +
- - + +
diff --git a/g4f/providers/base_provider.py b/g4f/providers/base_provider.py index 86789ec2..fa3f15a9 100644 --- a/g4f/providers/base_provider.py +++ b/g4f/providers/base_provider.py @@ -264,16 +264,18 @@ class AsyncGeneratorProvider(AsyncProvider): AsyncResult: An asynchronous generator yielding results. """ raise NotImplementedError() - + class ProviderModelMixin: default_model: str models: list[str] = [] model_aliases: dict[str, str] = {} - + @classmethod def get_models(cls) -> list[str]: + if not cls.models: + return [cls.default_model] return cls.models - + @classmethod def get_model(cls, model: str) -> str: if not model and cls.default_model is not None: -- cgit v1.2.3